<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html> <head>
<title>Hygienic Macros</title>
<script src="../../src/util/sets.js"></script>
<script src="../../src/util/freevars.js"></script>
<script src="../../src/util/alpharename.js"></script>
<script src="../../src/util/render.js"></script>
<script src="../../src/quasis/hygienicmacro.js"></script>
<script src="../../site/esparser/bundle.js"></script>
</head>

<body>

<style>
.running { background-color: yellow }
.pass { background-color: #afa }
.fail { background-color: #faa }
#tests td { font-family: monospace; white-space: pre }
</style>

<table id=tests>
  <tr><th>Quasi<th>Expanded</tr>
  <tr><td colspan=2>function cat(literals, substs) {
  function plus(a, b) { return ['BinaryExpr', { op: '+' }, a, b]; }
  function unesc(lit) {
    var s = lit[1].value;
    return ['LiteralExpr', { type: 'string', value: s.replace(/\\(.)/g, '$1') }];
  }
  var out = unesc(literals[0]);
  for (var i = 1, n = literals.length; i < n; ++i) {
    out = plus(out, plus(substs[i], unesc(literals[i])));
  }
  return out;
}</td>
  <tr><td>cat``<td>("")</tr>
  <tr><td>cat`foo`<td>("foo")</tr>
  <tr><th colspan=2>Free names in substitutions can appear free in output</tr>
  <tr><td>cat`foo${bar}baz`<td>"foo"+(bar+"baz")</tr>
  <tr><td>cat`foo${bar}baz$boo`<td>("foo"+(bar+"baz"))+(boo+"")</tr>
  <tr><th colspan=2>but other free names are rejected</tr>
  <tr><td colspan=2>function looseI(literals, substs) {
  var varI = ['IdExpr', { name: 'i' }];
  return ['ForStmt', {},
          ['AssignExpr', { op: '=' }, varI, substs[1]],
          ['BinaryExpr', { op: '<' }, varI, substs[2]],
          ['CountExpr', { op: '++', isPrefix: true }, varI],
          ['CallExpr', {}, substs[3], varI]];
}</tr>
  <tr><td>looseI`${0}-${n}:$count`<td>throws Error: Free variables [i]</tr>
  <tr><td colspan=2>function looseI2(literals, substs) {
  var varI = ['IdExpr', { name: 'i' }];
  var varN = ['IdExpr', { name: 'n' }];
  return ['CallExpr', {},
          ['FunctionExpr', {}, ['Empty'], ['ParamDecl', {}],
           ['ForStmt', {},
            ['BinaryExpr', { op: ',' },
             ['AssignExpr', { op: '=' }, varI, substs[1]],
             ['AssignExpr', { op: '=' }, varN, substs[2]]],
            ['BinaryExpr', { op: '<' }, varI, varN],
            ['CountExpr', { op: '++', isPrefix: true }, varI],
            ['CallExpr', {}, substs[3], varI]]]];
}</tr>
  <tr><td>looseI2`${0}-${n}:$count`<td>throws Error: Free variables [i, n]</tr>
  <tr><th colspan=2>Local names that conflict with free names are hygienized</tr>
  <tr><td colspan=2>function looseIFixed(literals, substs) {
  var varI = ['IdExpr', { name: 'i' }];
  var varN = ['IdExpr', { name: 'n' }];
  return ['CallExpr', {},
          ['FunctionExpr', {}, ['Empty'], ['ParamDecl', {}],
           ['ForStmt', {},
            ['VarDecl', {},
             ['InitPatt', {}, ['IdPatt', { name: 'i' }], substs[1]],
             ['InitPatt', {}, ['IdPatt', { name: 'n' }], substs[2]]],
            ['BinaryExpr', { op: '<' }, varI, varN],
            ['CountExpr', { op: '++', isPrefix: true }, varI],
            ['CallExpr', {}, substs[3], varI]]]];
}</tr>
  <tr><td>looseIFixed`${0}-${n}:${count}`</td>
      <td>(function (){for (var $0=0,$1=n;$0<$1;++$0)count($0)})()</tr>
  <tr><th colspan=2>Granting access to a property does not grant access to the object</tr>
  <tr><td colspan=2>function useProp(literals, substs) {
  return ['AssignExpr', {op:'='}, substs[1], ['LiteralExpr', {type:'number', value:1}]];
}</tr>
  <tr><td>useProp`${=x.y}`<td>x["y"]=1</tr>
  <tr><td colspan=2>function breakProp(literals, substs) {
  return ['AssignExpr', {op:'='}, ['IdExpr', { name: 'x' }], ['LiteralExpr', {type:'number', value:1}]];
}</tr>
  <tr><td>breakProp`${=x.y}`<td>throws Error: Free variables [x]</tr>
  <tr><th colspan=2>Malformed ASTs are rejected</tr>
  <tr><td colspan=2>function malAst(literals, substs) {
  return ['UnaryExpr', { op: 'alert("Hello, World!") * ' }, substs[1]];
}</tr>
  <tr><td>malAst`${1}`<td>throws Error: Malformed parse tree ["UnaryExpr",{"op":"alert(\"Hello, World!\") * "},["LiteralExpr",{"type":"number","value":1}]] produced {{ alert("Hello, World!") * 1 }} but cannot reconcile with {{ (alert("Hello, World!"))*1 }}</tr>
  <tr><th colspan=2>Assignment to read only capabilities rejected, but with '=' prefix, is allowed.</tr>
  <tr><td colspan=2>function lazySet1(literals, substs) {
  return ['FunctionExpr', {}, ['IdPatt', { name: 'f' }], ['ParamDecl', {}],
          ['ReturnStmt', {},
           ['AssignExpr', { op: '=' }, substs[1], substs[2]]]];
}</tr>
  <tr><td>lazySet1`${foo}$bar`<td>throws Error: ${foo} used in assignment.</tr>
  <tr><td>lazySet1`${=foo}$bar`<td>(function $0(){return foo=bar})</tr>
  <tr><td colspan=2>function lazySet2(literals, substs) {
  return ['FunctionExpr', {}, ['IdPatt', { name: 'f' }], ['ParamDecl', {}],
          ['ReturnStmt', {},
           ['AssignExpr', { op: '+=' }, substs[1], substs[2]]]];
}</tr>
  <tr><td>lazySet2`${foo}$bar`<td>throws Error: ${foo} used in assignment.</tr>
  <tr><td>lazySet2`${=foo}$bar`<td>(function $0(){return foo+=bar})</tr>
  <tr><td colspan=2>function lazyIncr(literals, substs) {
  return ['FunctionExpr', {}, ['IdPatt', { name: 'f' }], ['ParamDecl', {}],
          ['ReturnStmt', {},
           ['CountExpr', { isPrefix: true, op: '++' }, substs[1]]]];
}</tr>
  <tr><td>lazyIncr`${foo}`<td>throws Error: ${foo} used in assignment.</tr>
  <tr><td>lazyIncr`${=foo}`<td>(function $0(){return ++foo})</tr>
  <tr><td colspan=2>function lazyIter(literals, substs) {
  return ['FunctionExpr', {}, ['IdPatt', { name: 'f' }], ['ParamDecl', {}],
          ['ForInStmt', {}, substs[1], ['ObjectExpr', {}], ['BlockStmt', {}]]];
}</tr>
  <tr><td>lazyIter`${foo}`<td>throws Error: ${foo} used in assignment.</tr>
  <tr><td>lazyIter`${=foo}`<td>(function $0(){for (foo in {}){}})</tr>
  <tr><th colspan=2>Sometimes an expansion needs a little help from its friends</tr>
  <tr><td colspan=2>function needsHelp(literals, substs) {
  function str(s) { return ['LiteralExpr', { type: 'string', value: '' + s }]; }
  function plus(a, b) { return ['BinaryExpr', { op: '+' }, a, b]; }
  function unesc(lit) { return lit[1].value.replace(/\\(.)/g, '$1'); }
  function esc(ast) {
    return ['CallExpr', {}, ['MemberExpr', {}, substs[0], str('escape')], ast];
  }
  var out = str(unesc(literals[0]));
  for (var i = 1, n = literals.length; i < n; ++i) {
    out = plus(out, plus(esc(substs[i]), str(unesc(literals[i]))));
  }
  return out;
}</tr>
  <tr><td>needsHelp`===$foo===$bar===`</td>
      <td>("==="+((needsHelp["escape"](foo))+"==="))+((needsHelp["escape"](bar))+"===")</tr>
  <tr><th colspan=2>Handlers can't return or jump to containing labels</tr>
  <tr><td colspan=2>function returner(literals, substs) {
  return ['ReturnStmt', {}];
}</tr>
  <tr><td>returner`${foo}`</td><td>throws Error: Free labels [return]</tr>
  <tr><td colspan=2>function breaker(literals, substs) {
  return ['BreakStmt', { label: 'foo' }];
}</tr>
  <tr><td>breaker`${foo}`</td><td>throws Error: Free labels [foo]</tr>
</table>

<script>(function () {
  if (!Date.now) { Date.now = function () { return +(new Date); }; }

  function appendDiff(parent, expected, actual) {
    parent.appendChild(document.createTextNode(' ; was\n'));
    var eLen = expected.length, aLen = actual.length;
    var minLen = Math.min(eLen, aLen);
    var commonPrefix = 0;
    while (commonPrefix < minLen
           && expected.charCodeAt(commonPrefix)
              === actual.charCodeAt(commonPrefix)) {
      ++commonPrefix;
    }
    var maxSuffixLen = minLen - commonPrefix;
    var commonSuffix = 0;
    while (commonSuffix < maxSuffixLen
           && expected.charCodeAt(eLen - commonSuffix - 1)
              === actual.charCodeAt(aLen - commonSuffix - 1)) {
      ++commonSuffix;
    }
    parent.appendChild(document.createTextNode(
        actual.substring(0, commonPrefix)));
    var b = document.createElement('b');
    b.appendChild(document.createTextNode(
        actual.substring(commonPrefix, aLen - commonSuffix)));
    parent.appendChild(b);
    parent.appendChild(document.createTextNode(
        actual.substring(aLen - commonSuffix)));
  }

  var testsCont = document.getElementById('tests');
  var tests = testsCont.getElementsByTagName('tr');
  var group = function () {};
  var groupEnd = function () {};
  var log = function () {};
  if (typeof console !== 'undefined' && console.group) {
    group = function (name) { console.group(name); };
    groupEnd = function () { console.groupEnd(); };
    log = function (var_args) { console.log.apply(console, arguments); };
  }
  function innerText(el) {
    return el.innerText || el.firstChild.nodeValue;
  }
  function check(cell, actual) {
    var expected = innerText(cell);
    if (actual === expected) {
      cell.className = 'pass';
      return true;
    } else {
      cell.className = 'fail';
      appendDiff(cell, expected, actual);
      return false;
    }
  }

  var handlers = {};

  var i = 0, n = tests.length;
  function doTest() {
    if (i == n) { return; }
    setTimeout(doTest, 100);
    var test = tests[i++];

    var cells = test.getElementsByTagName('td');
    if (!cells.length) { return; }  // a header row

    var code = innerText(cells[0]);
    if (/^\s*function\b/.test(code)) {
      var qfn = eval('(0,' + code + ')');
      var qfnName = ('' + qfn).match(/^function\s+([^\s\x28]*)/)[1];
      handlers[qfnName] = qfn;
      console.log('found handler ' + qfnName);
      return;
    }

    test.className = 'running';
    group(code);
    var m = code.match(/^\s*(\w+)`([\s\S]*)`\s*$/);

    // Parse a simplified quasi grammar.
    var literalPortions = [], substitutions = [];
    var qfnName = m[1];
    var qfn = handlers[qfnName];
    var lastLit = m[2].replace(/((?:[^$]|\$\\)*)\$(\{[^\x7b]*\}|\w+)/g,
        function (_, lit, sub) {
          literalPortions.push(lit);
          substitutions.push('(' + sub.replace(/^\{|\}$/g, '') + ')');
          return '';
        });
    literalPortions.push(lastLit);

    try {
      var passed = true;
      var expanded;
      try {
        expanded = hygienicMacro(qfnName, qfn, literalPortions, substitutions);
      } catch (e) { expanded = 'throws ' + e; }
      passed &= check(cells[1], expanded);
      if (passed) {
        test.className = 'pass';
      } else {
        test.className = 'fail';
      }
    } finally {
      groupEnd();
    }
  }
  doTest();
})()</script>

<hr>
<address></address>
<!-- hhmts start --> Last modified: Fri Jan 15 13:12:38 PST 2010 <!-- hhmts end -->
</body> </html>
